﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.ServiceModel;
using Collab.Services.Remote.Contracts;
using Collab.Services.Remote.Entities;

namespace Collab.Services.Remote
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, AddressFilterMode = AddressFilterMode.Any, UseSynchronizationContext=false)]
    public class RemoteCollaborationService : IRemoteCollaborationService, IRemoteCollaborationHost
    {
        private readonly Dictionary<IRemoteCollaborationCallback, User> subscribers = new Dictionary<IRemoteCollaborationCallback, User>();
        private readonly User host;
        private readonly Subject<BufferUpdate> changeReceivedSubject = new Subject<BufferUpdate>();
        private readonly Subject<User> userConnectedSubject = new Subject<User>();
        private readonly Subject<User> userDisconnectedSubject = new Subject<User>();
        private readonly Subject<UserStatus> userStatusReceivedSubject = new Subject<UserStatus>();
        private readonly Subject<FileRequest> fileRequestReceivedSubject = new Subject<FileRequest>();
        private readonly Subject<AddedFileInformation> fileAddedSubject = new Subject<AddedFileInformation>();
        private readonly Subject<FileChunk> fileChunkReceivedSubject = new Subject<FileChunk>();
        private readonly Func<string, DocumentSnapshot> synchronizationFactory;
        private readonly Func<Solution> solutionInformationFactory;
        private readonly Dictionary<IRemoteCollaborationCallback, int> changeIndexes = new Dictionary<IRemoteCollaborationCallback, int>();
        
        public IObservable<User> WhenUserConnected { get { return this.userConnectedSubject.AsObservable(); } }
        public IObservable<User> WhenUserDisconnected { get { return this.userDisconnectedSubject.AsObservable(); } }
        public IObservable<BufferUpdate> WhenChangeReceived { get { return changeReceivedSubject.AsObservable(); } }
        public IObservable<FileRequest> WhenFileRequestReceived { get { return fileRequestReceivedSubject.AsObservable(); } }
        public IObservable<UserStatus> WhenUserStatusReceived  { get { return this.userStatusReceivedSubject.AsObservable(); } }
        public IObservable<AddedFileInformation> WhenFileAdded { get { return this.fileAddedSubject.AsObservable(); } }
        public IObservable<FileChunk> FileChunkReceived { get { return this.fileChunkReceivedSubject.AsObservable(); } }

        public RemoteCollaborationService(User host, Func<string, DocumentSnapshot> synchronizationFactory, Func<Solution> solutionInformationFactory)
        {
            Debug.Assert(host != null);
            Debug.Assert(!string.IsNullOrWhiteSpace(host.Name));
            Debug.Assert(host.Id != null);
            Debug.Assert(synchronizationFactory != null);
            Debug.Assert(solutionInformationFactory != null);
            this.host = host;
            this.synchronizationFactory = synchronizationFactory;
            this.solutionInformationFactory = solutionInformationFactory;
        }

        #region IRemoteCollaborationService Members

        public void Subscribe(User user)
        {
            Debug.Assert(user != null);
            Debug.Assert(!string.IsNullOrWhiteSpace(user.Name));
            Debug.Assert(user.Id != null);

            IRemoteCollaborationCallback callback = OperationContext.Current.GetCallbackChannel<IRemoteCollaborationCallback>();
            Debug.Assert(callback != null);
            lock (this.subscribers)
            {
                if (!this.subscribers.ContainsKey(callback))
                {
                    this.subscribers.Add(callback, user);
                    this.changeIndexes.Add(callback, 0);
                    this.CallUsers(this.subscribers.Where(c => c.Key != callback), x => x.RaiseUserConnected(user));
                    this.userConnectedSubject.OnNext(user);
                }
            }
        }

        public void Unsubscribe(User user)
        {
            Debug.Assert(user != null);
            Debug.Assert(!string.IsNullOrWhiteSpace(user.Name));
            Debug.Assert(user.Id != null);

            IRemoteCollaborationCallback callback = OperationContext.Current.GetCallbackChannel<IRemoteCollaborationCallback>();
            Debug.Assert(callback != null);
            lock (this.subscribers)
            {
                List<KeyValuePair<IRemoteCollaborationCallback, User>> usersToRemove = new List<KeyValuePair<IRemoteCollaborationCallback, User>>();

                if (this.subscribers.ContainsKey(callback))
                {
                    this.changeIndexes.Remove(callback);
                    this.subscribers.Remove(callback);
                    this.CallUsers(this.subscribers, x => x.RaiseUserDisconnected(user));
                    this.userDisconnectedSubject.OnNext(user);
                }
            }
        }

        public void SendChange(Entities.BufferUpdate change)
        {
            this.changeReceivedSubject.OnNext(change);
        }

        public void PublishChange(Entities.BufferUpdate change)
        {
            lock (this.subscribers)
            {
                this.CallUsers(this.subscribers.Where(subscriber => subscriber.Value.Id != change.Sender), x => {
                    change.UpdateIndex = this.changeIndexes[x]++;
                    x.RaiseChangePublished(change);
                });
            }
        }

        public IList<User> GetCurrentUsers()
        {
            IRemoteCollaborationCallback callback = OperationContext.Current.GetCallbackChannel<IRemoteCollaborationCallback>();

            var list = new List<User>();
            lock (this.subscribers)
            {
                list = this.subscribers.Where(s => s.Key != callback).Select(s => s.Value).ToList();
            }
            list.Add(this.host);
            return list;
        }

        public DocumentSnapshot GetCurrentRevision(string documentName)
        {
            var item = this.synchronizationFactory.Invoke(documentName);
            return item;
        }

        public Solution GetSolutionInformation()
        {
            return this.solutionInformationFactory.Invoke();
        }

        public void RequestFile(FileRequest fileRequest)
        {
            Debug.WriteLine("File Requested 1:" + fileRequest.FileName);
            this.fileRequestReceivedSubject.OnNext(fileRequest);
        }

        public void PublishFileChunk(FileChunk fileChunk)
        {
            lock (this.subscribers)
            {
                this.CallUsers(this.subscribers.Where(subscriber => subscriber.Value.Id == fileChunk.Target), x => x.RaiseFileChunkReceived(fileChunk));
            }
        }

        public void OpenedSolution(Solution solution)
        {
            lock (this.subscribers)
            {
                this.CallUsers(this.subscribers, x => x.RaiseOpenedSolution(solution));
            }
        }

        private void CallUsers(IEnumerable<KeyValuePair<IRemoteCollaborationCallback, User>> subscribers, Action<IRemoteCollaborationCallback> action)
        {
            List<KeyValuePair<IRemoteCollaborationCallback, User>> usersToRemove = new List<KeyValuePair<IRemoteCollaborationCallback, User>>();
            subscribers
            .Run(subscriber =>
                {
                    try
                    {
                        action.Invoke(subscriber.Key);
                    }
                    catch (Exception ex)
                    {
                        usersToRemove.Add(subscriber);
                    }
                }
            );
            usersToRemove.Run(subscriber =>
            {
                this.subscribers.Remove(subscriber.Key);
                this.CallUsers(this.subscribers, x => x.RaiseUserDisconnected(subscriber.Value));
                this.userDisconnectedSubject.OnNext(subscriber.Value);
            });
        }

        #endregion


        public void SendUserStatus(UserStatus status)
        {
            this.userStatusReceivedSubject.OnNext(status);
            lock (this.subscribers)
            {
                this.CallUsers(this.subscribers.Where(x => x.Value.Id != status.Sender), x => x.RaiseUserStatusReceived (status));
            }
        }


        public void PublishUserStatus(UserStatus status)
        {
            lock (this.subscribers)
            {
                this.CallUsers(this.subscribers, x => x.RaiseUserStatusReceived (status));
            }
        }

        public void SendFileAdded(AddedFileInformation fileInformation)
        {
            this.fileAddedSubject.OnNext(fileInformation);
        }


        public void PublishFileAdded(AddedFileInformation fileInformation)
        {
            lock (this.subscribers)
            {
                this.CallUsers(this.subscribers.Where(sub => sub.Value.Id != fileInformation.UserId), x => x.RaiseFileAdded(fileInformation));
            }
        }

        public void SendFileChunk(FileChunk fileChunk)
        {
            this.fileChunkReceivedSubject.OnNext(fileChunk);
        }


        public void PublishFileRequested(FileRequest request, Guid userId)
        {
            lock (this.subscribers)
            {
                this.CallUsers(this.subscribers.Where(sub => sub.Value.Id == userId), x => x.RaiseFileRequested(request));
            }
        }
    }
}
